/*
 * $Id$
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.struts2.osgi;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

import javax.servlet.ServletContext;

import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.osgi.loaders.VelocityBundleResourceLoader;
import org.apache.struts2.osgi.util.DtdUtil;
import org.apache.struts2.views.velocity.VelocityManager;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.FileManagerFactory;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.PackageProvider;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.DomHelper;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

/**
 * Struts package provider that starts the OSGi container and deelgates package
 * loading
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class OsgiConfigurationProvider implements PackageProvider,
		BundleListener {
	private static final Logger LOG = LoggerFactory
			.getLogger(OsgiConfigurationProvider.class);

	private Configuration configuration;
	private ObjectFactory objectFactory;
	private FileManager fileManager;

	private OsgiHost osgiHost;
	private BundleContext bundleContext;
	private BundleAccessor bundleAccessor;
	private boolean bundlesChanged = true;
	private ServletContext servletContext;

	public void init(Configuration configuration) throws ConfigurationException {
		osgiHost = (OsgiHost) servletContext
				.getAttribute(StrutsOsgiListener.OSGI_HOST);
		bundleContext = osgiHost.getBundleContext();
		bundleAccessor.setBundleContext(bundleContext);
		bundleAccessor.setOsgiHost(osgiHost);
		this.configuration = configuration;

		// this class loader interface can be used by other plugins to lookup
		// resources
		// from the bundles. A temporary class loader interface is set during
		// other configuration
		// loading as well
		servletContext.setAttribute(
				ClassLoaderInterface.CLASS_LOADER_INTERFACE,
				new BundleClassLoaderInterface());
	}

	public synchronized void loadPackages() throws ConfigurationException {
		if (LOG.isTraceEnabled())
			LOG.trace("Loading packages from XML and Convention on startup");

		// init action contect
		ActionContext ctx = ActionContext.getContext();
		if (ctx == null) {
			ctx = new ActionContext(new HashMap());
			ActionContext.setContext(ctx);
		}

		Set<String> bundleNames = new HashSet<String>();

		// iterate over the bundles and load packages from them
		Map<String, List<Bundle>> bundleMap = new TreeMap<String, List<Bundle>>(
				new Comparator<String>() {

					@Override
					public int compare(String o1, String o2) {
						return o1.compareTo(o2);
					}
				});
       
		for (Bundle bundle : bundleContext.getBundles()) {
			String bundleName = bundle.getSymbolicName();
			if (shouldProcessBundle(bundle)
					&& !bundleNames.contains(bundleName)) {
				bundleNames.add(bundleName);
				InputStream is1 = null;
				try {
					URL url=bundle.getResource("struts.xml");
					if(url==null){
						continue;
					}
					is1 = url.openStream();
					if(is1==null){
						continue;
					}
					InputSource in = new InputSource(is1);
					Document doc1 = DomHelper.parse(in, DtdUtil.getDtdMap());
					String order1 = parseOrder(doc1.getDocumentElement()
							.getAttribute("order"));
					List<Bundle> list = bundleMap.get(order1);
					if (list == null) {
						list = new ArrayList<Bundle>();
						bundleMap.put(order1, list);
					}
					list.add(bundle);
				} catch (IOException e) {
					e.printStackTrace();
				} finally {
					if (is1 != null) {
						try {
							is1.close();
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}

			}
		}

		for (String order : bundleMap.keySet()) {
			List<Bundle> list = bundleMap.get(order);
			for (Bundle b : list) {
				loadConfigFromBundle(b);
			}
		}

		bundlesChanged = false;
		bundleContext.addBundleListener(this);
	}

	private String parseOrder(String order) {
		if (null == order) {
			return "0";
		}
		if ("".equals(order)) {
			return "0";
		}
		return order;

	}

	/**
	 * Loads XML config as well as Convention config from a bundle Limitation:
	 * Constants and Beans are ignored on XML config
	 */

	protected void loadConfigFromBundle(Bundle bundle) {
		String bundleName = bundle.getSymbolicName();
		if (LOG.isDebugEnabled()) {
			LOG.debug("Loading packages from bundle [#0]", bundleName);
		}

		// init action context
		ActionContext ctx = ActionContext.getContext();
		if (ctx == null) {
			ctx = new ActionContext(new HashMap());
			ActionContext.setContext(ctx);
		}

		try {
			// the Convention plugin will use BundleClassLoaderInterface from
			// the ActionContext to find resources
			// and load classes
			ctx.put(ClassLoaderInterface.CLASS_LOADER_INTERFACE,
					new BundleClassLoaderInterface());
			ctx.put(BundleAccessor.CURRENT_BUNDLE_NAME, bundleName);

			if (LOG.isTraceEnabled()) {
				LOG.trace("Loading XML config from bundle [#0]", bundleName);
			}

			// XML config

			PackageLoader loader = configuration.getContainer().getInstance(
					PackageLoader.class);
			// new BundlePackageLoader();
			for (PackageConfig pkg : loader.loadPackages(bundle, bundleContext,
					objectFactory, fileManager,
					configuration.getPackageConfigs())) {
				configuration.addPackageConfig(pkg.getName(), pkg);
				bundleAccessor.addPackageFromBundle(bundle, pkg.getName());
			}

			// Convention
			// get the existing packages before reloading the provider (se we
			// can figure out what are the new packages)
			Set<String> packagesBeforeLoading = new HashSet(
					configuration.getPackageConfigNames());

			PackageProvider conventionPackageProvider = configuration
					.getContainer().getInstance(PackageProvider.class,
							"convention.packageProvider");
			if (conventionPackageProvider != null) {
				if (LOG.isTraceEnabled())
					LOG.trace("Loading Convention config from bundle [#0]",
							bundleName);
				conventionPackageProvider.loadPackages();
			}

			Set<String> packagesAfterLoading = new HashSet(
					configuration.getPackageConfigNames());
			packagesAfterLoading.removeAll(packagesBeforeLoading);
			if (!packagesAfterLoading.isEmpty()) {
				// add the new packages to the map of bundle -> package
				for (String packageName : packagesAfterLoading)
					bundleAccessor.addPackageFromBundle(bundle, packageName);
			}

			if (this.configuration.getRuntimeConfiguration() != null) {
				// if there is a runtime config, it meas that this method was
				// called froma bundle start event
				// instead of the initial load, in that case, reload the config
				this.configuration.rebuildRuntimeConfiguration();
			}
		} finally {
			ctx.put(BundleAccessor.CURRENT_BUNDLE_NAME, null);
			ctx.put(ClassLoaderInterface.CLASS_LOADER_INTERFACE, null);
		}
	}

	/**
	 * Checks for "Struts2-Enabled" header in the bundle
	 */
	protected boolean shouldProcessBundle(Bundle bundle) {
		String strutsEnabled = (String) bundle.getHeaders().get(
				OsgiHost.OSGI_HEADER_STRUTS_ENABLED);

		return "true".equalsIgnoreCase(strutsEnabled);
	}

	public synchronized boolean needsReload() {
		return bundlesChanged;
	}

	@Inject
	public void setObjectFactory(ObjectFactory factory) {
		this.objectFactory = factory;
	}

	@Inject
	public void setBundleAccessor(BundleAccessor acc) {
		this.bundleAccessor = acc;
	}

	@Inject
	public void setVelocityManager(VelocityManager vm) {
		Properties props = new Properties();
		props.setProperty("osgi.resource.loader.description",
				"OSGI bundle loader");
		props.setProperty("osgi.resource.loader.class",
				VelocityBundleResourceLoader.class.getName());
//		props.setProperty(Velocity.RESOURCE_LOADER,
//				"strutsfile,strutsclass,osgi");
		vm.setVelocityProperties(props);
	}

	@Inject
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
	}

	public void setFileManagerFactory(FileManagerFactory fmFactory) {
		this.fileManager = fmFactory.getFileManager();
	}

	public void destroy() {
		try {
			osgiHost.destroy();
		} catch (Exception e) {
			if (LOG.isErrorEnabled())
				LOG.error("Failed to stop OSGi container", e);
		}
	}

	/**
	 * Listens to bundle event to load/unload config
	 */
	public void bundleChanged(BundleEvent bundleEvent) {
		Bundle bundle = bundleEvent.getBundle();
		String bundleName = bundle.getSymbolicName();
		if (bundleName != null && shouldProcessBundle(bundle)) {
			switch (bundleEvent.getType()) {
			case BundleEvent.STARTED:
				if (LOG.isTraceEnabled())
					LOG.trace(
							"The bundlde [#0] has been activated and will be scanned for struts configuration",
							bundleName);
				loadConfigFromBundle(bundle);
				break;
			case BundleEvent.STOPPED:
				onBundleStopped(bundle);
				break;
			}
		}
	}

	/**
	 * This method is called when a bundle is stopped, so the config that is
	 * related to it is removed
	 * 
	 * @param bundle
	 *            the bundle that stopped
	 */
	protected void onBundleStopped(Bundle bundle) {
		Set<String> packages = bundleAccessor.getPackagesByBundle(bundle);
		if (!packages.isEmpty()) {
			if (LOG.isTraceEnabled())
				LOG.trace(
						"The bundle [#0] has been stopped. The packages [#1] will be disabled",
						bundle.getSymbolicName(),
						StringUtils.join(packages, ","));
			for (String packageName : packages)
				configuration.removePackageConfig(packageName);
		}
	}
}
